<template>
  <div
    ref="containerRef"
    :class="['grid-table', { 'table-border': border }]"
    :style="{ fontSize: fontSize }"
  >
    <!-- 表头 -->
    <header
      v-if="!notHeader"
      class="table-header"
      :style="{
        gridTemplateColumns: getGridTemplateColumns(),
        minHeight: currentHeight,
        textAlign: align,
        backgroundColor: colorConfig.header,
      }"
    >
      <div
        v-for="(item, colIndex) of computedColRules"
        :key="colIndex"
        :class="['table-cell', { 'col-border': border }]"
      >
        <span class="cell-text">{{ item.label }}</span>
      </div>
    </header>

    <!-- 表身 -->
    <main>
      <!-- 有数据时显示 -->
      <div
        v-if="computedTableData.length"
        class="table-body"
        :style="{
          gridTemplateColumns: getGridTemplateColumns(),
          minHeight: currentHeight,
          textAlign: align,
          backgroundColor: colorConfig.body,
        }"
      >
        <!-- 循环单元格 -->
        <div
          v-for="(cell, cellIndex) of computedCellData"
          :key="cellIndex"
          :class="['table-cell', { 'col-border': border }]"
          :style="{
            gridRow: cell.rowSpan ? `span ${cell.rowSpan}` : 'auto',
            gridColumn: cell.colSpan ? `span ${cell.colSpan}` : 'auto',
          }"
        >
          <span v-if="!slots.includes(cell.prop)" class="cell-text">{{
            cell.value
          }}</span>
          <slot v-else :row="cell" />
        </div>
      </div>
      <!-- 无数据时显示 -->
      <div
        v-else
        :class="['table-empty', { 'empty-border': border }]"
        :style="{
          minHeight: currentHeight,
          backgroundColor: colorConfig.body,
        }"
      >
        {{ emptyText }}
      </div>
    </main>

    <!-- 合计 -->
    <footer
      v-if="computedCount.length"
      class="table-footer"
      :style="{
        gridTemplateColumns: getGridTemplateColumns(),
        minHeight: currentHeight,
        textAlign: align,
        backgroundColor: colorConfig.footer,
      }"
    >
      <div
        v-for="(item, colIndex) of computedCount"
        :key="colIndex"
        :class="['table-cell', { 'col-border': border }]"
      >
        <span class="cell-text">{{ item }}</span>
      </div>
    </footer>
  </div>

  <li v-for="item of list" :key="item.id" :data-id="item.id">
    <div>{{ item.ddd }}</div>
  </li>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, toRefs } from 'vue';

const props = defineProps({
  /**
   * 表格数据
   */
  data: {
    type: Array,
    default: () => [],
  },

  /**
   * 包含：label, prop, width等属性
   * width如果为 -1 或者不传，则表示弹性宽度，如果是固定数值，则为固定px值
   */
  colRules: {
    type: Array,
    default: () => [],
  },

  /**
   * 表格尺寸，同element-plus属性一致，默认default
   * large, default, small
   */
  size: {
    type: String,
    default: 'default',
  },

  /**
   * 是否有边框
   */
  border: {
    type: Boolean,
    default: false,
  },

  /**
   * 文字对齐方式
   * left, center, right
   */
  align: {
    type: String,
    default: 'center',
  },

  /**
   * 颜色配置，包含 header 表头, body 表身, footer 合计 三个属性
   */
  colorConfig: {
    type: Object,
    default: () => ({
      header: '#f8f8f8',
      body: '#fff',
      footer: '#f5f8ff',
    }),
  },

  /**
   * 直接传入 index ，直接会按照百分比的宽度展示序号
   * 如果传入具体数字，0 表示不展示序号，-1为百分比均分，其他无符号数字代表具体px宽度
   */
  index: {
    type: [Boolean, Number],
    default: false,
  },

  /**
   * 合计函数，同el-table表格使用方式一致
   */
  summaryMethod: {
    type: Function,
    required: false,
  },

  /**
   * 跨行合并，传入需要合并的prop数组，例如['name', 'age']
   * 传入后，会根据按照data顺序，对prop相同的数据进行合并
   */
  mergeRow: {
    type: Array,
    default: () => [],
  },

  /**
   * 跨列合并
   */
  mergeCol: {
    type: Function,
    required: false,
  },

  /**
   * 要使用的插槽名称数组，例如['name', 'age']
   */
  slots: {
    type: Array,
    default: () => [],
  },

  /**
   * 无数据时显示的文字，默认为“暂无数据”
   */
  emptyText: {
    type: String,
    default: '暂无数据',
  },

  /**
   * 不显示表头，默认false，显示表头
   */
  notHeader: {
    type: Boolean,
    default: false,
  },
});

const { border, align, colorConfig, slots, emptyText, notHeader } =
  toRefs(props);

/**
 * 序号字段名称
 */
const INDEX_NUM = 'indexNum';

/**
 * 字体大小
 */
const fontSize = ref('14px');

/**
 * 当前表格默认最小高度
 */
const currentHeight = ref('40px');

/**
 * 总盒子的宽度
 */
const containerWdith = ref(0);

/**
 * 总盒子的ref
 */
const containerRef = ref(null);

/**
 * 每列规则的计算属性
 */
const computedColRules = computed(() => {
  const res = [...props.colRules];

  // 对没有填写的宽度统一赋值 -1
  res.forEach((item) => {
    if (!item.width) {
      item.width = -1;
    }

    if (typeof item.width === 'string') {
      item.width = parseInt(item.width);
    }
    // property 方便合计使用的属性
    item.property = item.prop;
  });

  // 如果序号为true或者有值
  if (props.index) {
    res.unshift({
      label: '序号',
      prop: INDEX_NUM,
      pdfshow: true,
      width: props.index === true ? -1 : props.index,
    });
  }
  return res;
});

/**
 * 表格数据计算属性
 */
const computedTableData = computed(() => {
  const res = [...props.data];
  if (props.index) {
    res.forEach((item, index) => {
      item.indexNum = index + 1;
    });
  }

  return res;
});

/**
 * 表格 合计 计算属性
 */
const computedCount = computed(() => {
  // table没有数据，不展示合计行组件
  if (!props.data.length) return [];
  if (props.summaryMethod && typeof props.summaryMethod === 'function') {
    return props.summaryMethod({
      columns: computedColRules.value,
      data: computedTableData.value,
    });
  }
  return [];
});

/**
 * 获取每列的style样式
 * @param {number} index 索引
 */
const getColWidth = (index) => {
  const flexWidth = computedColRules.value.every((item) => item.width == -1);
  if (flexWidth) return '1fr';

  if (index < computedColRules.value.length) {
    const currentWidth = computedColRules.value[index]?.width || null;
    // 如果宽度为 -1，直接给1fr，否则就给具体的px值
    if (currentWidth == -1) return '1fr';
    return `${computedColRules.value[index].width}px`;
  }

  return '1fr';
};

/**
 * 获取表格的grid-template-columns样式字符串（宽度计算）
 */
const getGridTemplateColumns = () => {
  return computedColRules.value.map((_, index) => getColWidth(index)).join(' ');
};

/**
 * 补全数组里面的索引位，例如[1, 3] => [1, 2, 3]
 * @param arr 数组，例如：[1, 2] 或者 [3, 1]

 */
const completeIndex = (arr) => {
  const [a, b] = arr;
  let res = [];

  if (a <= b) {
    // 处理正序数组
    for (let i = a; i <= b; ++i) {
      res.push(i);
    }
  } else {
    // 处理倒序数组
    for (let i = a; i >= b; --i) {
      res.push(i);
    }
    res = res.reverse();
  }
  return res;
};

/**
 * 获取需要合并单元格行的索引值
 * @param {funtion} rowIndex
 */
const getMergeRows = (rowIndex) => {
  if (rowIndex) {
    const rowCount = computedTableData.value.length;

    // 去除重复的索引
    const mergeRows = new Set();

    if (Array.isArray(rowIndex) && rowIndex.length === 2) {
      // 补全索引位
      const indexList = completeIndex(rowIndex);

      // 如果是数组，则判断里面是否为有效数字，并且介于0和rowCount之间
      indexList.forEach((idx) => {
        if (typeof idx === 'number' && idx >= 0 && idx < rowCount) {
          mergeRows.add(idx);
        }
      });
    } else if (typeof rowIndex === 'function') {
      // 如果是函数，则将其转为键值对对象 {0: true, 1: false}这种
      const indexObj = {};
      computedTableData.value.forEach((row, index) => {
        indexObj[index] = rowIndex(index) || false;
      });

      // 值为true的key添加到mergeRows集合中
      Object.entries(indexObj).forEach(([idxStr, shouldMerge]) => {
        const idx = parseInt(idxStr);
        if (shouldMerge) {
          mergeRows.add(idx);
        }
      });
    }
    return Array.from(mergeRows);
  }
  return [];
};

/**
 * 将data数据处理为扁平化数组，用于合并单元格的展示。
 */
const computedCellData = computed(() => {
  // 行计数器
  const rowCount = computedTableData.value.length;
  // 列计数器
  const colCount = computedColRules.value.length;

  // 初始化2维数组，单元格初始化为 { rowspan: 1, colspan: 1 }
  const mergeInfo = Array.from({ length: rowCount }, () =>
    Array.from({ length: colCount }, () => ({ rowspan: 1, colspan: 1 }))
  );

  // 跨行合并单元格计算（纵向）
  if (Array.isArray(props.mergeRow) && props.mergeRow.length) {
    // 对判断有没有序号，如果有向前追究序号属性
    const mergeRowList = props.index
      ? [INDEX_NUM, ...props.mergeRow]
      : props.mergeRow;

    for (const prop of mergeRowList) {
      // 找到跨行合并单元格prop的列索引位
      const colIndex = computedColRules.value.findIndex(
        (item) => item.prop === prop
      );
      if (colIndex === -1) continue;

      let row = 0;
      while (row < rowCount) {
        // rowspan用于计算当前单元格跨越的行数
        let rowspan = 1;
        const currentRow = computedTableData.value[row];

        for (let nextRow = row + 1; nextRow < rowCount; ++nextRow) {
          const nextData = computedTableData.value[nextRow];

          const shouldMerge = mergeRowList.every((p) => {
            // 找出当前行和下一行相同属性列的数据是否相同，相同则认为是同一组数据，需要合并单元格。
            const index = computedColRules.value.findIndex(
              (item) => item.prop === p
            );
            return index !== -1 && currentRow[p] === nextData[p];
          });

          // 如果存在，则对rowspan累加，继续计算下一行，否则直接跳出循环
          if (shouldMerge) {
            rowspan++;
          } else {
            break;
          }
        }

        // 判断 rowspan 大于1，则对当前行当前列的单元格进行标记需要跨行数
        if (rowspan > 1) {
          mergeInfo[row][colIndex].rowspan = rowspan;
          // 将之前相同的单元格标记为 0，无需渲染
          for (let i = 1; i < rowspan; ++i) {
            mergeInfo[row + i][colIndex].rowspan = 0;
          }
        }

        // 注意，因为rowspan累加后，需要跳过已经合并的行数，而不是++row
        row += rowspan;
      }
    }
  }

  // 跨列合并单元格计算（横向）
  if (props.mergeCol && typeof props.mergeCol === 'function') {
    const [rowProp, rowIndex] = props.mergeCol(computedTableData.value);

    const mergeRows = getMergeRows(rowIndex);

    mergeRows.forEach((row) => {
      // Map的key为prop列属性，值为索引，方便后续查找列索引位置
      const indexMap = new Map();

      computedColRules.value.forEach((item, index) => {
        indexMap.set(item.prop, index);
      });

      let i = 0;
      // 循环要合并的列 prop 数组
      while (i < rowProp.length) {
        const startProp = rowProp[i];

        // 获取需要合并的起始列的索引位置
        const startColIndex = indexMap.get(startProp);

        // startColIndex会为0
        if (startColIndex === undefined || startColIndex >= colCount - 1) {
          ++i;
          continue;
        }

        const currentValue = computedTableData.value[row][startProp];

        // colspan 计算当前单元格跨越的列数
        let colspan = 1;
        for (let j = i + 1; j < rowProp.length; ++j) {
          // 找出下一个prop列的索引位置
          const nextProp = rowProp[j];
          const nextColIndex = indexMap.get(nextProp);

          // 如果下一个索引不存在，或者 +colspan 不是连续的列索引位置，则跳出循环
          if (
            nextColIndex === undefined ||
            nextColIndex !== startColIndex + colspan
          ) {
            break;
          }

          // 如果下一个单元格的值和当前值相同，则colspan累加1
          if (computedTableData.value[row][nextProp] === currentValue) {
            ++colspan;
          } else {
            break;
          }
        }

        // 判断 rowspan 大于1，则对当前行当前单元格进行标记需要跨列数
        if (colspan > 1) {
          mergeInfo[row][startColIndex].colspan = colspan;
          // 将之前相同的列单元格标记为 0，无需渲染
          for (let j = 1; j < colspan; ++j) {
            mergeInfo[row][startColIndex + j].colspan = 0;
          }
        }

        // 注意，因为colspan累加后，需要跳过已经合并的列数，而不是++i
        i += colspan;
      }
    });
  }

  return getMergeData(rowCount, colCount, mergeInfo);
});

/**
 * 获取合并后的单元格数据数组。
 * @param {number} rowCount 行个数
 * @param {number} colCount 列个数
 * @param {Array} mergeInfo 初始化的合并数组
 */
const getMergeData = (rowCount, colCount, mergeInfo) => {
  const res = [];
  for (let row = 0; row < rowCount; ++row) {
    for (let col = 0; col < colCount; ++col) {
      if (mergeInfo[row][col].rowspan > 0 && mergeInfo[row][col].colspan > 0) {
        res.push({
          value: computedTableData.value[row][computedColRules.value[col].prop],
          rowSpan: mergeInfo[row][col].rowspan,
          colSpan: mergeInfo[row][col].colspan,
          prop: computedColRules.value[col].prop,
          rowData: computedTableData.value[row],
        });
      }
    }
  }
  return res;
};

/**
 * 获取总盒子的宽度
 */
const updateContainerWidth = () => {
  if (containerRef.value) {
    containerWdith.value = containerRef.value.getBoundingClientRect().width;
  }
};

onMounted(() => {
  updateContainerWidth();
  window.addEventListener('resize', updateContainerWidth);
});

onUnmounted(() => {
  window.removeEventListener('resize', updateContainerWidth);
});
</script>
<style scoped lang="less">
.grid-table {
  width: 100%;
  box-sizing: border-box;
  color: #121212;

  .table-header {
    display: grid;
    font-weight: 700;
  }

  .col-border {
    border-right: 1px solid #ebeef5;
  }

  .table-cell {
    border-bottom: 1px solid #ebeef5;
    display: flex;
    align-items: center;
    padding: 4px 8px;
    min-width: 40px;
    overflow-wrap: break-word;
    word-break: break-all;
    .cell-text {
      flex: 1;
    }
  }

  .table-body {
    display: grid;
  }

  .table-footer {
    display: grid;
  }

  .table-empty {
    line-height: v-bind(currentHeight);
    text-align: center;
    color: #909399;
  }
}

.table-border {
  border-top: 1px solid #ebeef5;
  border-left: 1px solid #ebeef5;
}

.empty-border {
  border-right: 1px solid #ebeef5;
  border-bottom: 1px solid #ebeef5;
}
</style>
